const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;

// 匹配到的是<xxx 或 <div:xxx 自定义标签名 即匹配到开始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`);

// 匹配的是 </xxx> 即匹配到结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);

// 匹配的是属性，如 xxx = "xxx" 或 xxx = 'xxx'
const attribute =
  /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

// 匹配的是开始闭合标签，如<div> 或 <br />
const startTagClose = /^\s*(\/?)>/;

// 匹配到是表达式变量，如{{name}}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;

// 解析html
function parseHTML(html) {
  const ELEMENT_TYPE = 1;
  const TEXT_TYPE = 3;
  const stack = []; // 存放元素的数组
  let currentParent; // 指向栈中的最后一个
  let root; // 是否是根节点

  // 转为抽象语法树
  function createASTElement(tag, attrs) {
    return {
      tag,
      type: ELEMENT_TYPE,
      children: [],
      attrs,
      parent: null,
    };
  }

  // 处理开始标签
  function start(tag, attrs) {
    let node = createASTElement(tag, attrs); // 创建一个ast树节点
    // 判断是否是空树
    if (!root) {
      root = node; // 空树则是当前树的根节点
    }
    // 如果栈中最后一个有内容，则把当前节点的父亲节点赋值为栈的最后一个
    if (currentParent) {
      node.parent = currentParent; // 子节点记住了父节点
      currentParent.children.push(node); // 父节点的子节点数组也需要保存值
    }
    // currentParent为栈中最后一个
    stack.push(node);
    currentParent = node;
  }
  // 处理文本内容标签
  function chars(text) {
    // 去除空
    text = text.replace(/\s/g, "");
    // 文本直接放到当前指向节点中
    text &&
      currentParent.children.push({
        type: TEXT_TYPE,
        text,
        parent: currentParent,
      });
  }
  // 处理结束标签
  function end(tag) {
    // 弹出最后一个节点，该节点已结束，不能作为父节点的判断
    let node = stack.pop();
    currentParent = stack[stack.length - 1];
  }

  // 裁剪html
  function advance(n) {
    html = html.substring(n);
  }

  // 寻找开启标签
  function parseStartTag() {
    const start = html.match(startTagOpen);
    if (start) {
      const match = {
        tagName: start[1], // 标签名
        attrs: [], // 属性数组对象，保存id、class等
      };
      // 先把 <div 开始标签截取掉
      advance(start[0].length);

      // 如果不是开始标签的结束，则一直匹配
      let attr, end;
      while (
        !(end = html.match(startTagClose)) &&
        (attr = html.match(attribute))
      ) {
        // 此时 id="xxx" class="xxx" 都被删除，只剩 >
        advance(attr[0].length);
        match.attrs.push({
          name: attr[1],
          value: attr[3] || attr[4] || attr[5],
        });
      }
      // 此时把 > 删除
      if (end) {
        advance(end[0].length);
      }
      return match;
    }

    // 不是开始标签，返回假
    return false;
  }

  // 每解析一段，就删除一段，直到最后解析完毕。因此可以写一个while循环
  while (html) {
    // html最开始肯定是一个 < (vue2要求单个根目录的原因)
    let textEnd = html.indexOf("<");

    // 如果索引是0，则说明是个开始标签；不为0则说明是结束标签
    if (textEnd === 0) {
      const startTagMatch = parseStartTag();

      if (startTagMatch) {
        // startTagMatch :{
        //   attrs: [{name: 'id', value: 'app'}]
        //   tagName: "div"
        // }
        // 解析到开始标签
        start(startTagMatch.tagName, startTagMatch.attrs);
        continue;
      }

      let endTagMatch = html.match(endTag);
      if (endTagMatch) {
        end(endTagMatch[0]);
        advance(endTagMatch[0].length);
        continue;
      }
    }

    // 截取文本内容
    if (textEnd > 0) {
      let text = html.substring(0, textEnd);
      if (text) {
        // 解析到文本
        chars(text);
        advance(text.length);
      }
    }
  }

  return root;
}

// 格式化id、class、style等
function genProps(attrs) {
  let str = "";

  // 循环遍历数组对象，每一项都转为 name:value 的格式
  for (let i = 0; i < attrs.length; i++) {
    let attr = attrs[i];

    // 如果是style行内式，color:red;background:green 的形式
    if (attr.name === "style") {
      let obj = {};
      attr.value.split(";").forEach((item) => {
        let [key, value] = item.split(":");
        obj[key] = value;
      });
      attr.value = obj;
    }
    str += `${attr.name}:${JSON.stringify(attr.value)},`;
  }

  // 去除最后一个多余的逗号
  return `{${str.slice(0, -1)}}`;
}

function gen(child) {
  if (child.type === 1) {
    // 节点
    return codegen(child);
  } else {
    // 文本
    let text = child.text;
    if (!defaultTagRE.test(text)) {
      return `_v(${JSON.stringify(text)})`;
    } else {
      // c创建元素
      // v创建文本
      // s创建变量
      // _v(_s(name)+'hello+_s(name))
      let tokens = [];
      let match;
      let lastIndex = 0;
      defaultTagRE.lastIndex = 0; // 每次捕获后先把索引重置
      while ((match = defaultTagRE.exec(text))) {
        let index = match.index;

        // 不能单纯放 {{xxx}} 的结果，也要放文本。如{{name}} hello {{age}}，第一次匹配到{{name}}，第二次匹配到{{age}}。则hello的索引位置是最后一次匹配到的内容长度（即{{age}}）加上其索引，即为整个文本长度
        // 注意要添加 JSON.stringify 转为字符串的形式
        if (index > lastIndex) {
          tokens.push(JSON.stringify(text.slice(lastIndex, index)));
        }
        tokens.push(`_s(${match[1].trim()})`);
        lastIndex = index + match[0].length;
      }

      // 如果匹配结束索引比整体长度要小，说明模板字符串在前内容在后，如{{name}} hello，此时把后面所有文本放进去即可
      if (lastIndex < text.length) {
        tokens.push(JSON.stringify(text.slice(lastIndex)));
      }
      return `_v(${tokens.join("+")})`;
    }
  }
}

// 格式化孩子数组
function genChildren(children) {
  return children.map((child) => gen(child)).join(",");
}

function codegen(ast) {
  // 格式化孩子数组
  let children = genChildren(ast.children);

  let code = `_c('${ast.tag}', ${
    ast.attrs.length > 0 ? genProps(ast.attrs) : null
  }${ast.children.length ? `,${children}` : ""})`;

  return code;
}

export const compileToFunction = (template) => {
  // 1.将template转为ast语法树
  const ast = parseHTML(template);

  // 2.生成render方法（render方法执行后返回的结果就是虚拟 DOM）
  // render() {
  //   return _c('div', {id: 'app'}, _c('div', {style: {color: 'red'}}, _v(_s(name)+'hello'))), _c('span', undefined, _v(_s(age)))
  // }
  let code = codegen(ast);
  code = `with(this){return ${code}}`;
  let render = new Function(code);

  return render;
};
